ЛЕКЦИЯ 7
Адреса и указатели.
Статически выделяемая память
Для
того, чтобы лучше понять специфику динамически
выделяемой памяти, рассмотрим сначала ее "антипод" - память,
распределяемую статически.
Такое
выделение памяти используется всякий раз при объявлении "обычных"
переменных в разделе var. Каждая переменная обладает
двумя атрибутами: именем и описанием.
var a: integer;
Описание
переменной нужно для того, чтобы компилятор знал, сколько ячеек памяти
необходимо выделить для ее хранения. Память под статическую переменную
выделяется один раз (до начала работы программы), и затем до конца работы
выделенная область памяти считается "занятой" - никакая другая
информация не может быть записана в эту ячейку.
Имя
переменной позволяет обращаться в процессе работы программы к той ячейке
памяти, которая была выделена под эту переменную на этапе компиляции.
Адреса
Имя
переменной является ее своеобразным (буквенным) адресом. Однако у любой
переменной есть также и обычный (цифровой или физический) адрес: номер ячейки,
выделенной под эту переменную.
При
страничной организации памяти адреса являются составными и состоят из номера
сегмента памяти и смещения ячейки относительно начала этого сегмента.
Лучшая
иллюстрация страничной организации памяти компьютера - это страничная
организация любой печатной книги. Для того чтобы найти нужную строчку, нет
необходимости задавать ее номер, считая от начала текста. Вместо этого можно
задать сначала номер страницы (= сегмент) и только затем номер строки, считая
от начала этой страницы (= смещение).
Для
обращения к статически заданной переменной можно использовать как ее имя,
объявленное в разделе var, так и ее физический адрес.
Например,
"адрес" одной и той же географической точки можно записать
по-разному: "49°47' северной широты и 86°36' восточной долготы" или
просто "вершина пика Белуха Восточная".
Указатели
Для
того чтобы хранить (цифровые) адреса, нужны особые переменные. Их называют
указателями и относят к специальному типу данных.
Описание
указателей
При
описании типизированного указателя необходимо сообщить компилятору, адреса переменных какого типа он может хранить:
var <имя_указателя>: ^<тип_адресуемой_переменной>;
Например:
var p: ^integer;
q: ^real;
s: ^array[1..10] of byte;
Кроме
того, существуют универсальные нетипизированные
указатели, которые могут хранить адрес переменной любого типа:
var <имя_указателя>: pointer;
Операции
с указателями
Определение
адреса
Физический
адрес любой переменной можно узнать при помощи стандартной функции addr(<имя_переменной>):<указатель>
или унарной операции @<имя_переменной>.
В зависимости от значения директивы компилятора {$T}, результатом
операции @ будет либо типизированный указатель (если установлено {$T+}), тип
которого будет определен в соответствии с типом использованной переменной, либо
нетипизированный указатель pointer
(если установлено {$T-}).
Результат
функции addr() совместим с указателями любых типов:
p:= addr(x); {x: real; p: ^byte)
Разыменование
Для
того чтобы воспользоваться значением, хранящимся по некоторому адресу,
необходимо его оттуда "извлечь". Унарная операция ^ называется
разыменованием и записывается по следующему шаблону:
<имя_указателя>^
Результатом
операции ^ является значение, хранящееся по указанному адресу. Тип этого значения
будет определяться типом (типизированного) указателя. К нетипизированным
указателям операцию разыменования применять нельзя.
Из-за вольностей, допускаемых процедурой addr(),
при разыменовании порой могут возникнуть забавные ситуации. Например, в результате
выполнения такой вот программы:
const a:
array[1..3] of char ='ААА'; {код(А)=128 или
01000000}
var p: ^word;
begin p:= addr(a);
writeln(p^)
end
на экран будет выведено 32896, что в двоичной системе счисления
выглядит как 01000000.01000000 (точкой помечена граница двух байтов). Иными
словами, коды двух первых букв оказались слитыми в значение типа word.
Замечание:
Операции @ и ^ являются взаимно обратными, то есть для любой переменной a и для
любого типизированного указателя p верны следующие равенства:
@(p^)=
p и (@a)^ = a
Присваивания
Для
указателей действуют гораздо более жесткие правила совместимости типов, чем для
обычных переменных. В операции присваивания могут фигурировать только
указатели, адресующие переменные одинаковых типов данных. Нельзя, скажем,
записать
p:= q; {p:= ^integer;
q: ^byte}
Обойти
эти ограничения позволяет универсальность нетипизированного
указателя pointer, совместимого с указателями любых
типов:
{p:=
^integer; q: ^byte; t: pointer}
t:=
q;
p:=
t;
У
указателей также существует свой "ноль", который означает, что
указатель не указывает никуда:
p:=
nil;
Замечание:
Если указатель не хранит конкретного адреса (его значение не определено), то
это вовсе не означает, что он никуда не указывает. Скорее всего, он все-таки
указывает, но только в какую-нибудь неожиданную (хорошо, если не системную)
область памяти.
Сравнения
Для
указателей определены две операции сравнения: = и <>.
Две
переменные, описанные как указатели на один и тот же тип данных, считаются
совпадающими, если они указывают на одну и ту же область памяти.
Для
разнотипных указателей сравнения невозможны: попытка записать
if p = q then writeln('yes'); {p: ^byte; q: ^integer}
вызовет
ошибку уже на этапе компиляции.
Однако
сравнивать типизированный и нетипизированный
указатели можно.
Динамически распределяемая память
Поскольку
к любой переменной можно обратиться двояко - по имени и по адресу, - есть
возможность сократить эту избыточность и оставить только один способ. Как мы
уже видели, наличие имени означает и наличие адреса. Иными словами, если вы
дали переменной имя в разделе var, то в процессе
компиляции у нее появится и адрес.
Итак,
пусть у некоторой переменной нет имени. Тем не менее
можно расположить ее в памяти, выделив под нее необходимое количество байтов, и
т.д. У переменной будет адрес, будет значение, но не будет имени.
Следовательно, обратиться к такой переменной можно будет только с помощью
указателя.
"Безымянные"
переменные отличаются от "нормальных" переменных:
Нет
имени - нечего описывать в разделе var.
Ничего
не описано, значит, на этапе компиляции память под переменную не выделена.
Следовательно, необходима возможность выделять память (и отменять это
выделение) прямо в процессе работы программы. Именно из-за этой гибкости такие
переменные и называют динамическими.
Если "потерять" указатель на переменную, то переменная
останется недоступным "мусором", занимая место в памяти, вплоть до
конца работы программы.
Динамическое выделение памяти
Типизированные
указатели
Для
выделения памяти служит стандартная процедура new():
new(<имя_указателя>);
Эта
процедура ищет в незанятой памяти подходящий по размеру кусок и,
"застолбив" это место для безымянной динамической переменной,
записывает в типизированный указатель адрес выделенного участка. Поэтому часто
говорят, что процедура new() создает динамическую
переменную. Размер выделяемого "куска памяти" напрямую зависит от
типа указателя.
Например,
если переменная p была описана как указатель на integer-переменную, то
процедура new(p) выделит два байта; под
real-переменную необходимо выделить четыре байта и т.д.
Нетипизированные указатели
Для
того чтобы выделить память, на которую будет указывать нетипизрованный
указатель pointer, нужно воспользоваться стандартной
процедурой getmem(p: pointer;
size: word), которая
выделит столько байт свободной памяти, сколько указано в переменной size.
Динамическое освобождение памяти
Типизированные
указатели
Для
уничтожения динамической переменной, то есть для освобождения занимаемой ею
памяти, предназначена стандартная процедура
dispose(<имя_типизир_указателя>).
Процедура
dispose() снимает пометку "занято" с
определенного количества байтов, начиная с указанного адреса. Эта область
памяти в дальнейшем считается свободной (хотя старое значение бывшей переменной
в ней может некоторое время еще оставаться). Количество освобождаемых байтов
определяется типом указателя p.
В
результате освобождения памяти при помощи процедуры dispose()
значение указателя, хранившего адрес освобожденной области, становится
неопределенным. Во избежание проблем его лучше сразу же "обнулить":
dispose(p);
p:=
nil;
Нетипизированные указатели
Для
того чтобы освободить память, на которую указывает нетипизрованный
указатель, нужно воспользоваться стандартной процедурой freemem(p:
pointer; size: word), которая освободит в памяти столько байтов (начиная с
указанного в переменной p адреса), сколько задано в переменной size.